Xcode 16.0 beta 3 を使っている場合に 1回の `collectionView(_, cellForItemAth:)` で、2回目の`dequeueReusableCell` を実行するとアプリがクラッシュする

Xcode 16.0 beta 3 を使っている場合に 1回の `collectionView(_, cellForItemAth:)` で、2回目の`dequeueReusableCell` を実行するとアプリがクラッシュする

Clock Icon2024.07.24

現在開発中のアプリが、先行して Xcode 16.0 beta 3で、ビルドできるのか調査をしたところ、Xcode 16.0 beta 3とCarthage 0.39.1との組み合わせでビルドができない問題があったものの、そこから先はアプリの起動まではすんなりいった。

しかし、ウォークスルー画面を抜けて、ホーム画面に遷移したところ、アプリがクラッシュしてしまった。このiOSアプリでは、ホーム画面で IGListKit 経由で UICollectionView を使って実装している。クラッシュする原因を調べたところ、Xcode 16.0 beta 3 と iOS 18 デバイスの組み合わせの場合に、UICollectionViewに問題があることがわかった。

検証環境

  • Xcode 16.0 beta 3
  • macOS Sonoma 14.5
  • iPhone 12 / iOS 18.0 Public Beta

問題: 1回の collectionView(_, cellForItemAth:) で、2回dequeueReusableCell を実行するとクラッシュする

前述の通り、アプリを起動するところまでは問題なかったが、ホーム画面に遷移するとアプリがクラッシュする問題が発生した。発生しているXcodeとiOSのバージョンの組み合わせを調べると以下のことがわかった。

  • Xcode 16.0 beta 3 + iOS 18:クラッシュする
  • Xcode 16.0 beta 3 + iOS 17.5:クラッシュしない
  • Xcode 15.2 + iOS 18:クラッシュしない

クラッシュ時に以下のログが出力されていた。

Expected dequeued view to be returned to the collection view in preparation for display. When the collection view's data source is asked to provide a view for a given index path, ensure that a single view is dequeued and returned to the collection view. Avoid dequeuing views without a request from the collection view. For retrieving an existing view in the collection view, use -[UICollectionView cellForItemAtIndexPath:] or -[UICollectionView supplementaryViewForElementKind:atIndexPath:].

また、当該箇所は以下のように実装していた。以下のコードは、IGListKit のサンプルコードをベースにした実装イメージである。

class LabelSectionController: ListSectionController {
  override func sizeForItem(at index: Int) -> CGSize {
    // 1回目のdequeue。セルの高さを取得している
    let cell = collectionContext!.dequeueReusableCell(of: MyCell.self, for: self, at: index)
    let height = cell.fittingSize().height
    return CGSize(width: collectionContext!.containerSize.width, height: height)
  }

  override func cellForItem(at index: Int) -> UICollectionViewCell {
    // 2回目のdequeue。実行時にアプリがクラッシュしてしまう
    return collectionContext!.dequeueReusableCell(of: MyCell.self, for: self, at: index)
  }
}

ブレークポイントを貼って処理を追いかけたところ、cellForItem(at:) で実行している dequeueReusableCell までは問題なく、次の cellForItem(at:) で実行している dequeueReusableCell を呼び出したあとにクラッシュすることが確認できた。

LabelSectionController にて、sizeForItem(at:)cellForItem(at:) のそれぞれでdequeueを呼んでいるようだ。 IGListKitにラッピングされていてわかりにくいが、1回の collectionView(_, cellForItemAth:) のなかでこれらのメソッドを呼んでいることがわかった。

解決編

クラッシュ直前のログと、これらの現象から UICollectionView に、Xcode 16.0 beta 3 + iOS 18デバイスの組み合わせの場合のみに問題があるのではないかと仮説を立てた。

Apple Developer Forumsを検索したところ、「Crash in iOS18」という投稿が見つかった。この投稿内でのやりとりで、1回の collectionView(_, cellForItemAth:) で、2回dequeueReusableCell を実行するとクラッシュすると書かれていた。手元で確認できた現象とも一致している。

以下のように、初回の描画時は任意のheightを返しておき、2回目以降はキャッシュした heightを返すようにしたところ、アプリはクラッシュしなくなった (以下実装例のためビルドに通らないかもしれない)。

class LabelSectionController: ListSectionController {
  private var cachedHieght: CGFloat?

  override func sizeForItem(at index: Int) -> CGSize {
    return CGSize(width: collectionContext!.containerSize.width, height: cachedHieght ?? 55)
  }

  override func cellForItem(at index: Int) -> UICollectionViewCell {
    let cell = collectionContext!.dequeueReusableCell(of: MyCell.self, for: self, at: index)
    cachedHieght = cell.fittingSize().height
    return cell
  }
}

まとめ

このUICollectionViewの仕様変更は影響を受けるアプリが多そうである。

Xcodeのメジャーバージョンの変更時期には、いつもbeta 6〜8まではリリースされるので、今回は応急措置をおこない、実際にどのように修正するかどうかについての検討については、Xcode 16.0 Release Candidate (RC) 以降でどのような挙動になるか確認してからにしたいと思う。

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.